/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2020-2020.
 * Description: oops enhance function
 * Author: huangshaobo
 * Create: 2020-7-22
 */

#include <linux/notifier.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/sysctl.h>
#include <linux/ptrace.h>
#include <linux/uaccess.h>
#include <linux/kdebug.h>
#include <linux/rtos_sysctl.h>
#include <linux/rtos_dump.h>

#ifdef CONFIG_HISI_WATCHDOG
#include <linux/hal/watchdog.h>
#endif

#ifdef CONFIG_X86
#include <linux/nmi.h>
#endif

#ifdef CONFIG_ARM64
#include <asm/system_misc.h>
#endif

static inline void oops_touch_watchdog(void)
{
#ifdef CONFIG_X86
	touch_nmi_watchdog();
#else
#ifdef CONFIG_HISI_WATCHDOG
	call_hwdog_handle();
#endif
#endif
}

enum {
	DUMP_MEM_REAR_INDEX,
	DUMP_MEM_FRONT_INDEX,
	INDEX_COUNT,
};

#define DUMP_MEM_MIN_SIZE (-512)
#define DUMP_MEM_MAX_SIZE 1024

static int dump_around_reg_mem_region[INDEX_COUNT] = {0, 0};
static const int dump_mem_min_size = DUMP_MEM_MIN_SIZE;
static const int dump_mem_max_size = DUMP_MEM_MAX_SIZE;

static const char *regs_names[] = {
#if defined(CONFIG_X86_32)
	"BX", "CX", "DX", "SI", "DI", "BP", "AX", "DS",
	"ES", "FS", "GS", "ORIG_AX", "IP", "CS", "FLAGS",
	"SP", "SS"
#elif defined(CONFIG_X86_64)
	"R15", "R14", "R13", "R12", "BP", "BX", "R11",
	"R10", "R9", "R8", "AX", "CX", "DX", "SI", "DI",
	"ORIG_AX", "IP", "CS", "FLAGS", "SP", "SS"
#elif defined(CONFIG_ARM)
	"R0", "R1", "R2", "R3", "R4", "R5", "R6", "R7",
	"R8", "R9", "R10", "FP", "IP", "SP", "LR", "PC"
#elif defined(CONFIG_ARM64)
	"X0", "X1", "X2", "X3", "X4", "X5", "X6", "X7",
	"X8", "X9", "X10", "X11", "X12", "X13", "X14",
	"X15", "X16", "X17", "X18", "X19", "X20", "X21",
	"X22", "X23", "X24", "X25", "X26", "X27", "X28",
	"X29", "LR", "SP", "PC"
#elif defined(CONFIG_PPC32) || defined(CONFIG_PPC64)
	"gpr[0]", "gpr[1]", "gpr[2]", "gpr[3]", "gpr[4]",
	"gpr[5]", "gpr[6]", "gpr[7]", "gpr[8]", "gpr[9]",
	"gpr[10]", "gpr[11]", "gpr[12]", "gpr[13]",
	"gpr[14]", "gpr[15]", "gpr[16]", "gpr[17]",
	"gpr[18]", "gpr[19]", "gpr[20]", "gpr[21]",
	"gpr[22]", "gpr[23]", "gpr[24]", "gpr[25]",
	"gpr[26]", "gpr[27]", "gpr[28]", "gpr[29]",
	"gpr[30]", "gpr[31]", "NIP", "MSR", "ORIG_GPR3",
	"CTR", "LINK", "XER", "CCR",
#if defined(CONFIG_PPC32)
	"MQ",
#else
	"SOFTE",
#endif
	"TRAP", "DAR", "DSISR", "RESULT"
#endif
};

static void show_pte_at_addr(unsigned long addr)
{
#if defined(CONFIG_ARM)
	struct mm_struct *mm;

	/* kernel addr use kernel page table */
	mm = (addr >= MODULES_VADDR) ? (&init_mm) : (current->mm);
	show_pte(KERN_ALERT, mm, addr);
#elif defined(CONFIG_ARM64)
	show_pte(addr);
#endif
}

void show_extra_register_data(struct pt_regs *regs, int left, int right)
{
	int names_size;
	int i;
	unsigned long *addr = NULL;
	if (left >= right)
		return;
	addr = (unsigned long *)regs;
	names_size = ARRAY_SIZE(regs_names);
	for (i = 0; i < names_size; i++) {
		unsigned int temp_data;
		unsigned int dup_addr = 0;
		int j;

		oops_touch_watchdog();

		/* skip invalid addr */
		if (get_kernel_nofault(temp_data, (const void *)addr[i])) {
			pr_info("%s:%px: Invalid kernel address.\n", regs_names[i], (void *)addr[i]);
			continue;
		}

		/* skip dup addr */
		for (j = 0; j < i; j++) {
			if (addr[i] == addr[j]) {
				pr_info("%s is same as %s,skip it.\n", regs_names[i], regs_names[j]);
				dup_addr = 1;
				break;
			}
		}
		if (dup_addr == 1)
			continue;

		do_show_data(addr[i] + left, right - left, regs_names[i]);
		show_pte_at_addr(addr[i]);
	}
	oops_touch_watchdog();
}

static int oops_enhance_die_handler(struct notifier_block *self,
		unsigned long val,
		void *data)
{
	struct die_args *args = data;
	struct pt_regs *regs = args->regs;
	switch (val) {
	case DIE_OOPS:
		if (panic_on_oops && (!user_mode(regs) || in_interrupt()))
			show_extra_register_data(regs,
					dump_around_reg_mem_region[DUMP_MEM_REAR_INDEX],
					dump_around_reg_mem_region[DUMP_MEM_FRONT_INDEX]);
		break;
	default:
		break;
	}
	return NOTIFY_OK;
}

static struct notifier_block oops_enhance_die_notifier = {
	.notifier_call = oops_enhance_die_handler,
	.priority = 100
};


static int proc_dump_reg_size(struct ctl_table *table, int write,
			void __user *buffer, size_t *lenp, loff_t *ppos)
{
	int ret;
	int range[INDEX_COUNT];
	struct ctl_table tmp = {
		.data = &range[0],
		.maxlen = sizeof(range),
		.mode = table->mode,
		.extra1 = (void *)&dump_mem_min_size,
		.extra2 = (void *)&dump_mem_max_size,
	};

	if (!write)
		return proc_dointvec_minmax(table, write, buffer, lenp, ppos);

	ret = proc_dointvec_minmax(&tmp, write, buffer, lenp, ppos);
	if (ret) {
		pr_err("At least one parameter is set incorrectly!");
		return ret;
	}

	if ((range[DUMP_MEM_REAR_INDEX] == 0) && (range[DUMP_MEM_FRONT_INDEX] == 0))
		goto out;

	if (range[DUMP_MEM_REAR_INDEX] >= range[DUMP_MEM_FRONT_INDEX]) {
		pr_err("dump_around_reg_mem_region set error!Max region is [%d ~ %d]\n",
			dump_mem_min_size, dump_mem_max_size);
		return -EINVAL;
	}

out:
	dump_around_reg_mem_region[DUMP_MEM_REAR_INDEX] = range[DUMP_MEM_REAR_INDEX];
	dump_around_reg_mem_region[DUMP_MEM_FRONT_INDEX] = range[DUMP_MEM_FRONT_INDEX];
	return 0;
}

static struct ctl_table oops_kernel_sysctls[] = {
	{
		.procname	= "dump_around_reg_mem_region",
		.data		= &dump_around_reg_mem_region[0],
		.maxlen		= sizeof(dump_around_reg_mem_region),
		.mode		= 0640,
		.proc_handler	= proc_dump_reg_size,
		.extra1		= (void *)&dump_mem_min_size,
		.extra2		= (void *)&dump_mem_max_size,
	},
	{}
};

struct ctl_table_header *g_oops_enhance_ctl_table;
int __init oops_enhance_init(void)
{
	/*
	 * register_die_notifier() currently has no failure branch
	 * so there is no need to introduce meaningless dead code and log
	 * strings to increase the size of the kernel image.
	 */
	register_die_notifier(&oops_enhance_die_notifier);

	/*
	 * The core function is to print detailed logs during OOPS.
	 * Whether the sysctl interface exists is not the most important.
	 * Therefore, do not give up the core function because the sysctl
	 * interface does not exist.
	 * In addition, register_sysctl() fails only when the memory is
	 * insufficient, which is a pseudo-proposition during system initialization.
	 */
	g_oops_enhance_ctl_table = register_sysctl("kernel", oops_kernel_sysctls);
	return  0;
}

void __exit oops_enhance_exit(void)
{
	if (g_oops_enhance_ctl_table)
		unregister_sysctl_table(g_oops_enhance_ctl_table);
	unregister_die_notifier(&oops_enhance_die_notifier);
}

module_init(oops_enhance_init);
module_exit(oops_enhance_exit);
MODULE_LICENSE("GPL");
